Building a Privacy-Preserving ML Service with Go and Intel SGX

February 4, 2024
See the code for this post on the confidential repository.

Building a Privacy-Preserving ML Service with Go and Intel SGX

Or: How I Learned to Stop Worrying and Love the Enclave

The Trust Problem Nobody Talks About

Picture this: You're a hospital with patient data that could help train a life-saving disease prediction model. There's a company with an amazing ML model that could use your data. Sounds like a match made in heaven, right?

Wrong. It's actually a match made in legal-nightmare-land.

You can't share your patient data (hello, HIPAA). They can't share their model (goodbye, competitive advantage). You're both standing there, arms crossed, like two kids who won't share their toys.

This is the "dual privacy problem," and it's been haunting the ML industry like that one bug you swear you fixed three sprints ago.

Enter Confidential Computing (Dramatic Music)

What if I told you there's a way for both parties to use each other's sensitive assets without either one actually seeing them?

No, I'm not talking about blockchain. Put down the whitepaper.

I'm talking about Intel SGX (Software Guard Extensions) – a technology that creates secure "enclaves" in your CPU where code and data are encrypted even while being processed. Not just at rest. Not just in transit. But actually while the CPU is crunching numbers.

It's like having a tiny, paranoid vault inside your processor that doesn't trust anyone – not the operating system, not the hypervisor, not even the person who owns the server. The only thing it trusts is math. And honestly? Same.

What We're Building

Today, we're building ConfidentialML – a privacy-preserving XGBoost inference service using:

  • Go – because we're not animals
  • EGo – a framework that makes SGX development actually pleasant
  • XGBoost – the ML algorithm that won't stop winning Kaggle competitions

Here's the magic trick:

  1. Model Owner uploads their proprietary XGBoost model → it gets sealed (encrypted) inside the enclave
  2. Data Owner sends their sensitive features → processed only inside the enclave
  3. Enclave runs inference → returns only the prediction
  4. Neither party ever sees the other's sensitive data
┌─────────────────────────────────────────────────────────────┐
│ SGX Enclave │
│ (The Trust Zone™) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Sealed │ │ XGBoost │ │ Attestation │ │
│ │ Model │───▶│ Inference │◀───│ Service │ │
│ │ 🔒 │ │ 🧠 │ │ 📜 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
▲ │ │
Model Owner Data Owner Verifier
(can't see data) (can't see model) (trusts math)

The Three Pillars of Confidential Computing

Before we dive into code, let's understand the three superpowers that SGX gives us:

1. Runtime Encryption 🔐

Normal encryption protects data at rest (on disk) and in transit (over the network). But what about when you're actually using the data? That's when it's most vulnerable – sitting there in RAM, naked and exposed.

SGX encrypts data in memory. Even if someone dumps your server's RAM (looking at you, cold boot attacks), they'll just see gibberish. The decryption keys never leave the CPU.

2. Sealing 📦

"Sealing" is SGX's way of saying "encrypt this data so only this specific enclave on this specific CPU can decrypt it."

When we seal our ML model, we're essentially putting it in a lockbox that can only be opened by our exact code running on that exact machine. Try to open it anywhere else? Nope. Different code? Nope. Same code but different machine? Still nope.

It's like a password manager, except the password is "being the right code on the right hardware."

3. Remote Attestation 🔍

Here's where it gets really cool. How does a client know they're talking to a genuine SGX enclave running the expected code?

Remote attestation. The enclave can generate a cryptographic proof that says:

  • "I am running inside a genuine Intel SGX enclave"
  • "My code has this exact hash (MRENCLAVE)"
  • "I was signed by this key (MRSIGNER)"

The client can verify this proof before sending any sensitive data. It's like checking someone's ID, except the ID is mathematically unforgeable.

Let's Build This Thing

Project Structure

confidential-ml/
├── cmd/
│ ├── server/ # The enclave server
│ └── client/ # Demo client with attestation
├── internal/
│ ├── api/ # HTTP handlers
│ ├── attestation/ # Remote attestation magic
│ ├── model/ # XGBoost inference
│ └── sealing/ # SGX sealing service
├── scripts/
│ └── train_model.py # Train our demo model
├── enclave.json # EGo configuration
└── Makefile # Because typing is hard

Step 1: The Sealing Service

First, let's build the service that encrypts our model using SGX sealing:

// internal/sealing/service.go
package sealing
import (
"github.com/edgelesssys/ego/ecrypto"
)
// Service provides sealing operations using EGo's ecrypto
type Service struct {
simulationMode bool
}
// Seal encrypts data using the enclave's product key.
// The sealed data can only be decrypted by the same enclave binary.
func (s *Service) Seal(plaintext []byte) ([]byte, error) {
if s.simulationMode {
// In simulation mode, use mock sealing for development
return s.mockSeal(plaintext), nil
}
// The real deal - uses hardware-derived keys
return ecrypto.SealWithProductKey(plaintext, nil)
}
// Unseal decrypts previously sealed data.
// This will fail if called from a different enclave or platform.
func (s *Service) Unseal(ciphertext []byte) ([]byte, error) {
if s.simulationMode {
return s.mockUnseal(ciphertext), nil
}
return ecrypto.Unseal(ciphertext, nil)
}

The beauty here is that SealWithProductKey uses a key derived from:

  • The enclave's identity (MRSIGNER)
  • The CPU's unique key
  • Some cryptographic fairy dust

Try to unseal this data on a different machine or with different code? The CPU will just shrug and say "I don't know her."

Step 2: The Model Manager

Now let's handle loading and managing our XGBoost model:

// internal/model/manager.go
package model
import (
"github.com/Elvenson/xgboost-go"
"github.com/example/confidential-ml/internal/sealing"
)
type Manager struct {
ensemble *inference.Ensemble
sealing *sealing.Service
modelPath string
// ... other fields
}
// LoadModel parses and loads an XGBoost model from JSON
func (m *Manager) LoadModel(jsonData []byte) error {
// Load the model using xgboost-go
ensemble, err := xgboost.LoadXGBoostFromJSONBytes(
jsonData,
"", // No feature map
numClasses, // 3 for Iris
maxDepth, // Tree depth
activationFn, // Softmax for classification
)
if err != nil {
return err
}
m.ensemble = ensemble
m.rawJSON = jsonData
return nil
}
// SaveSealed seals and persists the model to disk
func (m *Manager) SaveSealed() error {
return m.sealing.SealToFile(m.rawJSON, m.modelPath)
}
// LoadSealed loads a previously sealed model
func (m *Manager) LoadSealed() error {
jsonData, err := m.sealing.UnsealFromFile(m.modelPath)
if err != nil {
return err
}
return m.LoadModel(jsonData)
}

When the model owner uploads their model, we:

  1. Parse it to make sure it's valid
  2. Seal it using SGX
  3. Write the sealed blob to disk

The file on disk is completely useless without the enclave. You could email it to your competitors and they'd just see random bytes. (Please don't actually do this. Your security team will have questions.)

Step 3: The Inference Engine

Here's where the magic happens – running predictions without exposing the model:

// internal/model/inference.go
package model
type PredictionResult struct {
Value float64
Probability map[string]float64
ModelType ModelType
}
// Predict runs inference on a feature vector
func (e *InferenceEngine) Predict(features []float64) (*PredictionResult, error) {
// Get the model (it's decrypted only in enclave memory)
ensemble, err := e.manager.GetModel()
if err != nil {
return nil, errors.New("no model loaded")
}
// Validate feature count
expectedFeatures := e.manager.GetNumFeatures()
if len(features) != expectedFeatures {
return nil, fmt.Errorf(
"feature mismatch: expected %d, got %d",
expectedFeatures, len(features),
)
}
// Run XGBoost prediction
input := createSparseMatrix(features)
probs, err := ensemble.PredictProba(input)
if err != nil {
return nil, err
}
// Return only the prediction, not the model internals
return &PredictionResult{
Value: findMaxClass(probs),
Probability: formatProbabilities(probs),
ModelType: ModelTypeClassification,
}, nil
}

Notice what we're not doing:

  • We're not logging the input features (privacy!)
  • We're not exposing model weights (IP protection!)
  • We're not storing the data anywhere (compliance!)

The features come in, the prediction goes out, and everything in between stays encrypted in enclave memory.

Step 4: Remote Attestation

Now for the trust verification. How does a client know they're talking to the real deal?

// internal/attestation/service.go
package attestation
import (
"github.com/edgelesssys/ego/enclave"
)
type Report struct {
RawReport []byte
UniqueID []byte // MRENCLAVE - hash of enclave code
SignerID []byte // MRSIGNER - hash of signing key
ProductID uint16
SecurityVer uint16
IsSimulation bool
}
// GetReport generates an attestation report
func (s *Service) GetReport(userData []byte) (*Report, error) {
if s.isSimulation {
return s.getSimulatedReport(userData)
}
// Get the self report from the enclave
selfReport, err := enclave.GetSelfReport()
if err != nil {
return nil, err
}
// Get remote report with user data
remoteReport, err := enclave.GetRemoteReport(userData)
if err != nil {
return nil, err
}
return &Report{
RawReport: remoteReport,
UniqueID: selfReport.UniqueID,
SignerID: selfReport.SignerID,
ProductID: extractProductID(selfReport),
SecurityVer: uint16(selfReport.SecurityVersion),
IsSimulation: false,
}, nil
}

The UniqueID (MRENCLAVE) is particularly important – it's a hash of the enclave's code. If someone modifies even a single byte of the code, the hash changes, and clients will reject the connection.

It's like a tamper-evident seal, except instead of "void if removed," it's "void if you try anything funny."

Step 5: The API Layer

Let's wire everything together with a REST API:

// internal/api/handler.go
package api
// POST /model - Upload XGBoost model
func (h *Handler) handleModelUpload(w http.ResponseWriter, r *http.Request, requestID string) {
body, err := io.ReadAll(r.Body)
if err != nil {
h.writeError(w, 400, "PARSE_ERROR", "Failed to read request", requestID)
return
}
// Load the model
if err := h.modelManager.LoadModel(body); err != nil {
h.writeError(w, 400, "INVALID_MODEL", err.Error(), requestID)
return
}
// Seal and persist
if err := h.modelManager.SaveSealed(); err != nil {
h.writeError(w, 500, "SEALING_ERROR", "Failed to persist model", requestID)
return
}
// Return model info (but not the model itself!)
info, _ := h.modelManager.GetModelInfo()
json.NewEncoder(w).Encode(info)
}
// POST /predict - Run inference
func (h *Handler) handlePredict(w http.ResponseWriter, r *http.Request, requestID string) {
var req PredictRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.writeError(w, 400, "PARSE_ERROR", "Invalid JSON", requestID)
return
}
// Run inference (features never logged or persisted!)
result, err := h.inference.Predict(req.Features)
if err != nil {
h.writeError(w, 400, "INFERENCE_ERROR", err.Error(), requestID)
return
}
json.NewEncoder(w).Encode(result)
}
// GET /attestation - Get attestation report
func (h *Handler) handleAttestation(w http.ResponseWriter, r *http.Request, requestID string) {
report, err := h.attestation.GetReport(nil)
if err != nil {
h.writeError(w, 500, "ATTESTATION_ERROR", "Failed to generate report", requestID)
return
}
json.NewEncoder(w).Encode(AttestationResponse{
UniqueID: hex.EncodeToString(report.UniqueID),
SignerID: hex.EncodeToString(report.SignerID),
ProductID: report.ProductID,
SecurityVer: report.SecurityVer,
IsSimulation: report.IsSimulation,
})
}

Step 6: The Demo Client

Finally, let's build a client that demonstrates the full flow:

// cmd/client/main.go
func (c *Client) RunDemo() {
fmt.Println("╔════════════════════════════════════════════════════════════╗")
fmt.Println("║ ConfidentialML - Private XGBoost Inference Demo ║")
fmt.Println("╚════════════════════════════════════════════════════════════╝")
// Step 1: Health check
fmt.Println("📋 Step 1: Checking server health...")
health, _ := c.GetHealth()
fmt.Printf(" ✅ Server is %s\n", health.Status)
fmt.Printf(" 🔒 Enclave mode: %s\n", health.EnclaveMode)
// Step 2: Verify attestation
fmt.Println("🔐 Step 2: Verifying enclave attestation...")
attest, _ := c.GetAttestation()
fmt.Printf(" 📜 Unique ID: %s...\n", attest.UniqueID[:16])
fmt.Printf(" ✍️ Signer ID: %s...\n", attest.SignerID[:16])
if attest.IsSimulation {
fmt.Println(" ⚠️ Running in SIMULATION mode")
} else {
fmt.Println(" ✅ Running in real SGX enclave")
}
// Step 3: Run inference
fmt.Println("🧠 Step 3: Running private inference...")
fmt.Println(" Sending features: [5.1, 3.5, 1.4, 0.2]")
result, _ := c.Predict([]float64{5.1, 3.5, 1.4, 0.2})
fmt.Printf(" ✅ Prediction: %.0f\n", result.Prediction)
fmt.Println(" 📈 Probabilities:", result.Probability)
fmt.Println("\n✅ Demo complete!")
fmt.Println(" The model owner never saw your data.")
fmt.Println(" You never saw the model weights.")
fmt.Println(" Privacy preserved! 🎉")
}

Running the Demo

Let's see this thing in action!

1. Train a Demo Model

# Create virtual environment and install dependencies
python3 -m venv venv
./venv/bin/pip install xgboost scikit-learn numpy
# Train the model
./venv/bin/python scripts/train_model.py

Output:

============================================================
ConfidentialML - Demo Model Training
============================================================
📊 Loading Iris dataset...
Features: ['f0', 'f1', 'f2', 'f3']
Classes: ['setosa', 'versicolor', 'virginica']
Samples: 150
🤖 Training XGBoost classifier...
Trained 50 rounds
📊 Model Evaluation:
Accuracy: 1.0000
💾 Exporting model to data/iris_model.json...
Model saved (64,522 bytes)
✅ Model training complete!

2. Start the Server

# Build and run in simulation mode (no SGX hardware needed)
go build -o confidential-ml ./cmd/server
./confidential-ml

Output:

{"level":"INFO","msg":"Starting ConfidentialML server","port":"8080"}
{"level":"WARN","msg":"Running in SIMULATION mode - for development only"}
{"level":"INFO","msg":"Server listening","addr":":8080"}

3. Upload the Model

curl -X POST http://localhost:8080/model \
-H "Content-Type: application/json" \
--data-binary @data/iris_model.json

Response:

{
"num_features": 4,
"num_trees": 150,
"model_type": "classification",
"num_classes": 3
}

The model is now sealed inside the enclave. The file on disk (sealed_model.bin) is encrypted and useless outside this specific enclave.

4. Run Inference

curl -X POST http://localhost:8080/predict \
-H "Content-Type: application/json" \
-d '{"features": [5.1, 3.5, 1.4, 0.2]}'

Response:

{
"prediction": 0,
"probability": {
"0": 0.9939,
"1": 0.0045,
"2": 0.0017
},
"model_type": "classification"
}

The model predicted class 0 (setosa) with 99.39% confidence. And at no point did:

  • The server log your input features
  • You see the model weights
  • Anyone have access to unencrypted data outside the enclave

5. Run the Full Demo

go build -o client ./cmd/client
./client --mode demo

Output:

╔════════════════════════════════════════════════════════════╗
║ ConfidentialML - Private XGBoost Inference Demo ║
╚════════════════════════════════════════════════════════════╝
📋 Step 1: Checking server health...
✅ Server is healthy
🔒 Enclave mode: simulation
📦 Model loaded: true
🔐 Step 2: Verifying enclave attestation...
📜 Unique ID (MRENCLAVE): 3130be0a0da1ef13...
✍️ Signer ID (MRSIGNER): 0530a8fe59fed475...
🏭 Product ID: 1
🔢 Security Version: 1
⚠️ Running in SIMULATION mode (development only)
🧠 Step 3: Running private inference...
Sending Iris flower features: [5.1, 3.5, 1.4, 0.2]
(These features stay encrypted in transit and memory)
✅ Prediction: 0
📊 Model type: classification
📈 Class probabilities:
- Class 0: 0.9939
- Class 1: 0.0045
- Class 2: 0.0017
════════════════════════════════════════════════════════════
✅ Demo complete! The model owner never saw your data,
and you never saw the model weights. Privacy preserved!
════════════════════════════════════════════════════════════

6. See What Happens When Trust Fails

./client --mode trust-failure

Output:

╔════════════════════════════════════════════════════════════╗
║ Trust Failure Demonstration ║
╚════════════════════════════════════════════════════════════╝
This demo shows what happens when a client has incorrect
expectations about the enclave identity.
📜 Actual enclave Unique ID: 9282306c0dd15186...
❌ Client expects Unique ID: 0000000000000000...
🚫 TRUST VERIFICATION FAILED!
The enclave's identity does not match what we expected.
This could mean:
• The server is running different code than expected
• The server is not running in a genuine SGX enclave
• A man-in-the-middle attack is occurring
⛔ A security-conscious client would REFUSE to send
sensitive data in this situation!

This is the beauty of remote attestation – if anything is off, the client knows immediately.

Real-World Applications

This isn't just a cool demo. Here are some actual use cases:

Healthcare 🏥

  • Hospital sends patient vitals → gets disease risk prediction
  • Patient data never leaves the hospital's control
  • Model IP stays protected
  • HIPAA compliance maintained

Finance 💰

  • Bank sends customer data → gets credit score
  • Proprietary scoring model stays secret
  • Customer data isn't exposed to model provider
  • Regulatory requirements satisfied

Insurance 📋

  • Insurer gets risk assessment
  • Pricing algorithm stays confidential
  • Customer data protected
  • Both parties happy (rare in insurance)

Federated Learning 🤝

  • Multiple parties contribute to model training
  • No one sees anyone else's data
  • Model improves without data sharing
  • Everyone wins

Limitations and Considerations

Before you go rewriting your entire ML infrastructure, some caveats:

Performance

SGX enclaves have limited memory (typically 128MB-256MB). Large models might not fit. There's also some overhead for encryption/decryption, though it's usually negligible for inference.

Side-Channel Attacks

SGX isn't perfect. Researchers have found side-channel attacks (Spectre, Meltdown, etc.) that can leak information. Intel keeps patching these, but it's an ongoing cat-and-mouse game.

Hardware Requirements

Real SGX requires Intel CPUs with SGX support. The good news is that EGo's simulation mode lets you develop without the hardware. The bad news is that simulation mode doesn't provide actual security guarantees.

Complexity

There's a learning curve. You need to understand enclaves, attestation, sealing, and how they all fit together. Hopefully this blog helped with that!

Conclusion

We built a privacy-preserving ML inference service that solves the dual privacy problem:

  • Model owners can deploy their proprietary models without exposing the weights
  • Data owners can get predictions without exposing their sensitive data
  • Both parties can verify they're dealing with a genuine, unmodified enclave

The code is available on GitHub (link in the description), and you can run it in simulation mode without any special hardware.

Confidential computing is still a relatively new field, but it's growing fast. As more organizations deal with sensitive data and ML, solutions like this will become increasingly important.

Now if you'll excuse me, I need to go seal some secrets. 🔐


Resources


Did you find this helpful? Have questions about confidential computing? Drop a comment below or find me on Twitter [@yourhandle]. And if you're building something cool with SGX, I'd love to hear about it!

See the code for this post on the confidential repository.